/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package pin.db.connectionPool;

import java.io.InputStream;
import java.io.PrintWriter;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.EventObject;
import java.util.LinkedList;
import java.util.List;
import java.util.Properties;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.sql.ConnectionPoolDataSource;
import javax.sql.PooledConnection;

/**
 *
 * @author shoaly
 */
public class PinDataSource implements ConnectionPoolDataSource, Runnable {

    public void stop() {
        stopping = true;
        for (Connection conn : idleList) {
            try {
                conn.close();
            } catch (SQLException ex) {
                Logger.getLogger(PinDataSource.class.getName()).log(Level.SEVERE, null, ex);
            }
        }
        for (PinConnection conn : activeList) {
            try {
                conn.getConnection().close();
            } catch (SQLException ex) {
                Logger.getLogger(PinDataSource.class.getName()).log(Level.SEVERE, null, ex);
            }
        }
        for (PinConnection conn : sweepList) {
            try {
                conn.getConnection().close();
            } catch (SQLException ex) {
                Logger.getLogger(PinDataSource.class.getName()).log(Level.SEVERE, null, ex);
            }
        }

    }
    private static Thread thread;
    private static boolean stopping = false;
    private static boolean started = false;
    private static boolean inited = false;
    List<PinConnectionEventListener> connectionEventListeners = new ArrayList<PinConnectionEventListener>();
    private static LinkedList<Connection> idleList = new LinkedList<Connection>();
    private static LinkedList<PinConnection> activeList = new LinkedList<PinConnection>();
    private static LinkedList<PinConnection> sweepList = new LinkedList<PinConnection>();
    private static Properties properties = null;
    private static String url = null;
    private static Properties info = null;
    private static int maxActive,  maxWait,  maxTimeOut,  minTimeOut,  maxIdle,  minIdle,  timeOut = maxTimeOut,  maxSweepInterval,  minSweepInterval,  interval = maxSweepInterval;

    public PinDataSource() throws SQLException {

    }

    private void checkInited() throws SQLException {
        if (!inited) {
            SQLException ex = new SQLException("not inited");
            Logger.getLogger(PinDataSource.class.getName()).log(Level.SEVERE, null, ex);
            throw ex;
        }
    }

    public void init(String configFile) throws SQLException {
        if (!inited) {
            try {
                doInit(configFile);
            } catch (NumberFormatException ex) {
                Logger.getLogger(PinDataSource.class.getName()).log(Level.SEVERE, null, ex);
                throw new SQLException(ex);
            }
        }
        if (!started) {
            thread = new Thread(this);
            thread.setPriority(thread.getPriority() + 1);
            thread.start();
            started = true;
        }
    }

    private void doInit(String configFile) throws SQLException {
        try {
            System.out.println(PinDataSource.class.getClassLoader().getResource("."));
            InputStream in = PinDataSource.class.getClassLoader().getResourceAsStream(configFile);
            properties = new Properties();
            properties.load(in);
            properties.list(System.out);
            String driverClassName = properties.getProperty("driverClassName");
            url = properties.getProperty("url");
            info = new Properties();
            info.setProperty("user", properties.getProperty("user"));
            info.setProperty("password", properties.getProperty("password"));
            String connectionProperties = properties.getProperty("connectionProperties");
            String[] propertiesString = connectionProperties.split("&");
            for (String p : propertiesString) {
                String key = p.split("=")[0];
                String value = p.split("=")[1];
                info.setProperty(key, value);
            }

            int initialSize = Integer.parseInt(properties.getProperty("initialSize"));
            maxActive = Integer.parseInt(properties.getProperty("maxActive"));
            maxWait = Integer.parseInt(properties.getProperty("maxWait"));
            maxIdle = Integer.parseInt(properties.getProperty("maxIdle"));
            minIdle = Integer.parseInt(properties.getProperty("minIdle"));
            minTimeOut = Integer.parseInt(properties.getProperty("minTimeOut"));
            maxTimeOut = Integer.parseInt(properties.getProperty("maxTimeOut"));
            timeOut = maxTimeOut;
            maxSweepInterval = Integer.parseInt(properties.getProperty("maxSweepInterval"));
            minSweepInterval = Integer.parseInt(properties.getProperty("minSweepInterval"));
            interval = maxSweepInterval;
            //maxPreparedStatements = Integer.parseInt(properties.getProperty("maxPreparedStatements"));


            Class.forName(driverClassName);
            increaseConnections(initialSize);

            inited = true;

        } catch (Exception ex) {
            Logger.getLogger(PinDataSource.class.getName()).log(Level.SEVERE, null, ex);
            throw new SQLException(ex);
        }

    }

    public void addConnectionEventListener(PinConnectionEventListener listener) {
        connectionEventListeners.add(listener);
    }

    public void removeConnectionEventListener(PinConnectionEventListener listener) {
        connectionEventListeners.remove(listener);
    }

    private void trigger(EventObject event) {
        if (event instanceof PinConnectionEvent) {
            PinConnectionEvent pinEvent = (PinConnectionEvent) event;
            for (PinConnectionEventListener listener : connectionEventListeners) {
                final int eventType = pinEvent.getEventType();
                switch (eventType) {
                    case PinConnectionEvent.EVENT_CLOSE:
                        listener.connectionClosed(pinEvent);
                        break;
                    case PinConnectionEvent.EVENT_CREATE:
                        listener.connectionCreated(pinEvent);
                        break;
                    case PinConnectionEvent.EVENT_DESTROY:
                        listener.connectionDestoried(pinEvent);
                        break;
                    case PinConnectionEvent.EVENT_OPEN:
                        listener.connectionOpened(pinEvent);
                        break;

                }
            }

        }
    }

    private void increaseConnections(int num) throws SQLException {
        for (int i = 0; i < num; i++) {

            Connection conn = DriverManager.getConnection(url, info);
            //PinConnectionEvent event = new PinConnectionEvent(conn, PinConnectionEvent.EVENT_CREATE);
            //trigger(event);
            idleList.push(conn);

        }
    }

    public void run() {
        try {
            checkInited();
        } catch (SQLException ex) {
            Logger.getLogger(PinDataSource.class.getName()).log(Level.SEVERE, null, ex);
            return;
        }
        while (!stopping) {

            try {
                Thread.sleep(interval);

            } catch (InterruptedException ex) {
                Logger.getLogger(PinDataSource.class.getName()).log(Level.SEVERE, null, ex);
            }
            try {
                sweepConnection();
            } catch (SQLException ex) {
                Logger.getLogger(PinDataSource.class.getName()).log(Level.SEVERE, null, ex);
            }
        }

    }

    public PinConnection getPooledConnection() throws SQLException {

        checkInited();
        long startTime = System.currentTimeMillis();
        while (System.currentTimeMillis() < startTime + maxWait) {
            synchronized (PinDataSource.class) {
                if (idleList.isEmpty()) {
                    if (activeList.size() < maxActive) {
                        increaseConnections(1);
                    }
                } else {
                    Connection conn = idleList.pop();
                    if (conn.isClosed()) {
                        increaseConnections(1);
                    } else {

                        PinConnection pconn = new PinConnection(conn, this);
                        activeList.push(pconn);
                        pconn.setStartTime(System.currentTimeMillis());
                        PinConnectionEvent event = new PinConnectionEvent(pconn, PinConnectionEvent.EVENT_OPEN);
                        trigger(event);
                        return pconn;
                    }
                }
            }
            try {
                Thread.sleep(100);
            } catch (InterruptedException ex) {
                Logger.getLogger(PinDataSource.class.getName()).log(Level.SEVERE, null, ex);
            }
        }
        SQLException ex = new SQLException("maxWait time out, can't get a connection now");
        trigger(new PinConnectionEvent(null, ex, PinConnectionEvent.EVENT_OPEN));
        throw ex;

    }

    public int getActive() throws SQLException {
        checkInited();
        return activeList.size();
    }

    public int getIdle() throws SQLException {
        checkInited();
        return idleList.size();
    }

    public synchronized void sweepConnection() throws SQLException {
        checkInited();
        if (getIdle() < minIdle) {
            interval = minSweepInterval;
            timeOut = minTimeOut;
        } else {
            interval = maxSweepInterval;
            timeOut = maxTimeOut;
        }
        //System.out.println(String.format("before sweep:%d/%d/%d interval:%d", getActive(), getIdle(), sweepList.size(), interval));
        for (PinConnection conn : activeList) {
            if (conn.getStartTime() + timeOut < System.currentTimeMillis()) {
                sweepList.push(conn);
            }
        }
        //System.out.println(String.format("after sweep:%d/%d/%d", getActive(), getIdle(), sweepList.size()));
        //if (idleList.size() < minIdle && sweepList.size() > 0) {
        recycleConnection();
        //}
        //System.out.println(String.format("after clean:%d/%d/%d", getActive(), getIdle(), sweepList.size()));
    }

    public synchronized void recycleConnection() throws SQLException {
        checkInited();
        //System.out.println(String.format("clean"));
        for (PinConnection conn : sweepList) {
            closeConnection(conn);
        }
        sweepList.clear();

    }

    public synchronized void closeConnection(PinConnection pconn) throws SQLException {
        checkInited();
        try {
            pconn.commit();
            trigger(new PinConnectionEvent(pconn, PinConnectionEvent.EVENT_CLOSE));
        } catch (Exception ex) {
            trigger(new PinConnectionEvent(pconn, new SQLException(ex), PinConnectionEvent.EVENT_CLOSE));
        }
        Connection conn = pconn.getConnection();
        pconn.conn = null;
        if (activeList.contains(pconn)) {
            activeList.remove(pconn);

        }
        if (idleList.size() < maxIdle) {
            idleList.push(conn);
        } else {
            try {
                conn.close();
                PinConnectionEvent event = new PinConnectionEvent(pconn, PinConnectionEvent.EVENT_DESTROY);
                trigger(event);
            } catch (SQLException ex) {
                Logger.getLogger(PinDataSource.class.getName()).log(Level.SEVERE, null, ex);
                trigger(new PinConnectionEvent(pconn, new SQLException(ex), PinConnectionEvent.EVENT_DESTROY));
            }
        }

    }

    public PooledConnection getPooledConnection(
            String user, String password) throws SQLException {
        throw new UnsupportedOperationException("Not supported yet.");
    }

    public PrintWriter getLogWriter() throws SQLException {
        throw new UnsupportedOperationException("Not supported yet.");
    }

    public int getLoginTimeout() throws SQLException {
        throw new UnsupportedOperationException("Not supported yet.");
    }

    public void setLogWriter(PrintWriter out) throws SQLException {
        throw new UnsupportedOperationException("Not supported yet.");
    }

    public void setLoginTimeout(int seconds) throws SQLException {
        throw new UnsupportedOperationException("Not supported yet.");
    }

    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        stop();
    }


}


